defmodule Pango.DeeprCypherV2 do
	@moduledoc """
	    match: match ...
	    optional: optional match...
	    可以在 match, optional 里面放入 自定义对象xxx, 便于不方便查询的对象可以查询, mod 里相应的要实现 get_match(xxx, target_obj)
	    attach: 把属性作为一个map 放在对象上, 如果为true, 则放在对象上; 如果为false, 则根据情况判定;默认为false
	    =>:
	        1. 对象 => [字段1, 字段2]
	        2. 对象 =>　%{"字段名" => true/false/别名}
	                非直接查询对象的 true/false 不起作用, 只有别名才起作用
	        3. 只能是查询的对象才能有这种写法
	            字段 =>  true/false/别名
	                true, 依附在对象上; false 不依附在对象上; 别名
	            attach 不管是true, false 都依附在对象上

	"""
	import MacroCandy
	import PrivateCandy

	def list(
				caller_mod,
				user,
				object,
				%{
					"=>" => select,
				} = args
			) do
		Neo4jLib.check_params_valid!(select)
		filter = Map.get(args, "()", %{})
		Neo4jLib.check_params_valid!(filter)
		(is_list(select) && select != []) || (Kernel.is_map(select) && map_size(select) != 0) || remind(__ENV__)
		page = Map.get(args, "[]", false)
		page == false || is_integer(page) || PrivateCandy.debug_remind(__ENV__, "deepr_cypher_v2_input_error")
		match = Map.get(args, "match", [])
		total_page = Map.get(args, "total_page", false)
		order_by = Map.get(args, "OrderBy", nil)
		is_boolean(total_page) || PrivateCandy.debug_remind(__ENV__, "deepr_cypher_v2_input_error")
		optional = Map.get(args, "optional", [])
		is_list(match) && is_list(optional) || PrivateCandy.debug_remind(__ENV__, "match OR optional must be list")
		target_mod = caller_mod.get_db_mod(object)
		filter = Map.put(filter, "OrderBy", order_by)
		get_list_cypher(
			object,
			user,
			target_mod,
			total_page,
			page,
			match,
			optional,
			select,
			Map.get(args, "attach", true),
			filter
		)
	end
	def list(_, _, _, _) do
		debug_remind(__ENV__, "缺少返回数据,格式:{=>: []} | =>:{a: {}, b: {}}")
	end

	def info(
				caller_mod,
				user,
				object,
				%{
					"()" => where,
					"=>" => select,
				} = args
			) do
		match = Map.get(args, "match", [])
		optional = Map.get(args, "optional", [])
		is_list(match) && is_list(optional) || PrivateCandy.debug_remind(__ENV__, "match OR optional must be list")
		target_mod = caller_mod.get_db_mod(object)
		get(user, target_mod, match, optional, select, where)
	end
	def info(_, _, _, _) do
		PrivateCandy.debug_remind(__ENV__, "缺少筛选条件或者返回数据,返回数据格式:{=>: []} | =>:{a: {}, b: {}}")
	end

	@doc """

	"""
	@spec get(struct | charlist, atom, list, list, map | list, map) :: charlist
	def get(uid_or_user, mod, match, optional, select, where) do
		target_obj = mod.deepr_object()
		{with_acc, select} = get_select(select, target_obj, mod, true)
		{filter, params, with1} = Pango.CypherFilter.where(mod, where, target_obj, optional)
		do_filter = if filter != "" do
			filter_with = [target_obj | (match ++ with1)]
										|> Enum.uniq()
										|> Enum.join(",")
			"""
			WITH #{filter_with}
			#{filter}
			"""
		else
			""
		end
		with = [target_obj | with_acc]
					 |> Enum.join(",")
		{
			get_belong_str(uid_or_user, mod, target_obj, where)
			<> "#{get_match(mod, match, with1, where)} " <> do_filter
			<> " WITH #{with}" <> " RETURN '#{target_obj}' AS object, #{select} AS entry limit 1",
			params
		}
	end

	def get_list_cypher(
				object,
				user,
				target_mod,
				total_page,
				page,
				match,
				optional,
				select,
				attach,
				where
			) do
		target_obj = object
		{with_acc, select} = get_select(select, object, target_mod, attach)
		belong_to = target_mod.belong_to()
		belong_label =
			# 自己本身是, Project
			if map_size(belong_to) == 0 do
				target_obj
			else
				belong_to.label
			end
		belong_obj = Macro.underscore(belong_label)
		with_acc = (iif?(total_page == false, [], [belong_obj]) ++ [target_obj | with_acc])
							 |> Enum.uniq()
							 |> Enum.join(",")
		{order, where} = case is_map(where) && Map.pop(where, "OrderBy") do
			{nil, where} ->
				{"", where}
			{order, where} ->
				if is_map(order) && map_size(order) > 0 do
					str = Enum.map_join(
						order,
						", ",
						fn ({key, v}) ->
							String.replace(key, "->", ".") <> " #{v}"
						end
					)
					{" ORDER BY " <> str, where}
				else
					{"", where}
				end
		end
		#		{filter, params, with} = Pango.CypherFilter.where(target_mod, where, target_obj, optional)
		#		with = with -- match
		#		do_filter = if filter != "" do
		#			filter_with = [belong_obj, target_obj | (match ++ with)]
		#										|> Enum.uniq()
		#										|> Enum.join(",")
		#			"""
		#			WITH #{filter_with}
		#			#{filter}
		#			"""
		#		else
		#			""
		#		end
		{filter, params, match} = Pango.CypherFilter.where(target_mod, where, target_obj, match)
		do_filter = if filter != "" do
			filter_with = [belong_obj, target_obj | (match ++ optional)]
										|> Enum.uniq()
										|> Enum.join(",")
			"""
			WITH #{filter_with}
			#{filter}
			"""
		else
			""
		end
		str = get_belong_str(user, target_mod, object, %{})
		<> "#{get_match(target_obj, target_mod, match -- optional, optional, %{})}"
			 #					<> "#{get_match(target_mod, match, with, %{})}"
					<> " #{do_filter} WITH #{with_acc}" <> order
		<> " RETURN #{inspect(object)} AS object" <> get_page_str(belong_obj, select, total_page, page)
		{str, params}
	end

	def get_belong_str(user, target_mod, target_obj, where) do
		project_id = if Kernel.is_map(user) do
			case UserLib.get_project_id(user) do
				nil ->
					""
				id ->
					id
			end
		else
			user
		end
		fields = target_mod.fields()
		case target_mod.belong_to() do
			%{dir: belong_dir, mod: belong_mod, label: belong_label} ->
				belong_obj = Macro.underscore(belong_label)
				case belong_mod.belong_to() do
					%{dir: belong_dir2, mod: belong_mod2, label: belong_label2} ->
						{primary_key, _} = belong_mod2.primary_key()
						belong_obj2 = belong_mod2.deepr_object()
						"MATCH (#{target_obj}:#{target_mod.label()}) " <>
						get_point(belong_dir, belong_obj, belong_label, "")
						<> " (:#{belong_mod.label()})"
						<> get_point(belong_dir2, Macro.underscore(belong_label2), belong_label2, "")
							 <> " (#{belong_mod2.deepr_object()}:#{belong_mod2.label()}{#{primary_key}: #{inspect(project_id)}})"
					_ ->
						{primary_key, _} = belong_mod.primary_key()
						is_visible = iif?(Map.get(fields, "visible") == nil, "", "{visible: true}")
						"MATCH (#{target_obj}:#{target_mod.label()}#{is_visible}) " <>
						get_point(belong_dir, belong_obj, belong_label, "")
						<> " (#{belong_mod.deepr_object}:#{belong_mod.label()}{#{primary_key}: #{inspect(project_id)}})"
						<> get_filter_str(where, belong_label, target_obj)
				end
			_ ->
				"MATCH (#{target_obj}:#{target_mod.label()}) "
				<> get_filter_str(where, nil, target_obj)
		end
	end


	def get_match(target_obj, target_mod, match, optional, _where) do
		fields = target_mod.fields()
		match_acc_fun = fn (acc, list, is_option) ->
			Enum.reduce(
				list,
				acc,
				fn (field_obj, {acc, appear_acc}) ->
					if field_obj not in appear_acc do
						case Map.get(fields, field_obj) do
							%{dir: rel_dir, label: rel_label, mod: rel_mod} ->
								rel_obj = Macro.underscore(rel_label)
								#								rel_obj = case Map.get(fields, rel_obj) do
								#									nil ->
								#										""
								#									_v ->
								#										rel_obj
								#								end
								str = " MATCH (#{target_obj})" <> get_point(rel_dir, rel_obj, rel_label, "")
								<> object(field_obj, rel_mod.label())
								str = iif?(is_option, " OPTIONAL " <> str, str)
								{[str | acc], [field_obj, rel_obj | appear_acc]}
							%{relation_object: obj} ->
								case Map.get(fields, obj) do
									%{dir: rel_dir, label: rel_label, mod: rel_mod} ->
										rel_obj = Macro.underscore(rel_label)
										str = " MATCH (#{target_obj})" <> get_point(rel_dir, rel_obj, rel_label, "")
										<> object(obj, rel_mod.label())
										str =
											if obj in optional || field_obj in optional do
												iif?(is_option, " OPTIONAL " <> str, str)
											else
												str
											end
										{[str | acc], [field_obj, obj | appear_acc]}
									_ ->
										{acc, appear_acc}
								end
							_ ->
								{acc, appear_acc}
						end
					else
						{acc, appear_acc}
					end
				end
			)
		end
		match_acc_fun.({[], []}, Enum.uniq(match), false)
		|> match_acc_fun.(Enum.uniq(optional), true)
		|> elem(0)
	end

	def get_match(target_mod, match, optional_match, _where) do
		fields = target_mod.fields()
		target_obj = target_mod.deepr_object()
		match_acc_fun = fn (acc, list, is_option) ->
			Enum.reduce(
				list,
				acc,
				fn (field_obj, acc) ->
					if !:erlang.function_exported(target_mod, :get_match, 6) do
						case Map.get(fields, field_obj) do
							%{dir: rel_dir, label: rel_label, mod: rel_mod} ->
								rel_obj = Macro.underscore(rel_label)
								rel_obj = case Map.get(fields, rel_obj) do
									nil ->
										""
									_v ->
										rel_obj
								end
								str = " MATCH (#{target_obj})" <> get_point(rel_dir, rel_obj, rel_label, "")
								<> object(field_obj, rel_mod.label())
								iif?(is_option, " OPTIONAL " <> str, str)
							%{relation_object: _} ->
								nil
							nil ->
								nil
							_ ->
								#								str = "MATCH (#{target_obj})#{field_obj}"
								#								iif?(is_option, " OPTIONAL " <> str, str)
								nil
						end
					else
						target_mod.get_match(field_obj, target_obj)
					end
					|> ListKit.append(acc)
				end
			)
		end
		match_acc_fun.([], Enum.uniq(match), false)
		|> match_acc_fun.(Enum.uniq(optional_match), true)
	end

	defp get_filter_str(where, rel_label, obj) do
		rel_filter = case rel_label != nil && Map.get(where, Macro.underscore(rel_label)) do
			false ->
				""
			nil ->
				""
			"" ->
				""
			str ->
				str
		end
		case Map.get(where, obj, nil) do
			nil ->
				if rel_filter == "" do
					""
				else
					" WHERE #{rel_filter} "
				end
			"" ->
				if rel_filter == "" do
					""
				else
					" WHERE #{rel_filter} "
				end
			str ->
				rel_filter = if rel_filter != "" do
					" AND #{rel_filter}"
				else
					""
				end
				" WHERE #{str} #{rel_filter}"
		end
	end

	@spec get_select(map | list, charlist, atom, boolean) :: {list, charlist}
	def get_select(select, target_obj, target_mod, attach) do
		fields = target_mod.fields()
		# acc1: select 是list的时候, 查询的属性
		# acc2: select 是map的时候, 查询的属性
		{acc1, kv_acc, rest_select, with_acc} = Enum.reduce(
			select,
			{[], [], select, []},
			fn
				{"sql" = key, fields}, {acc1, kv_acc, select_acc, with_acc} ->
					is_map(fields) || PrivateCandy.debug_remind(__ENV__, "constant的value 要为map,传入值:#{fields}")
					{kv_acc, with_acc} = Enum.reduce(
						fields,
						{kv_acc, with_acc},
						fn ({key2, value}, {fied_acc, with_acc2}) ->
							with_str = value <> " AS " <> key2
							kv_str = key2 <> ": " <> key2
							{[kv_str | fied_acc], [with_str | with_acc2]}
						end
					)
					{acc1, kv_acc, Map.delete(select_acc, key), with_acc}
				# value: 是list;
				# value: 是map
				{field_obj, value}, {acc1, kv_acc, select_acc, with_acc} = acc ->
					is_map? = is_map(value)
					field_values = map_join(value, ", ", field_obj)
					case Map.get(fields, field_obj) do
						# target 对象的属性
						nil when field_obj == target_obj ->
							kv_acc = ListKit.append(field_values, kv_acc)
							{acc1, kv_acc, Map.delete(select_acc, field_obj), with_acc}
						nil ->
							PrivateCandy.debug_remind(__ENV__, "#{target_obj} 没有#{field_obj}属性")
						# 对象是关系上的 (a)-[c:C]-(b) 也即是c
						%{relation_object: _} ->
							acc
						# 关系上的属性
						%{type: type, label: rel_label} ->
							rel_obj = Macro.underscore(rel_label)
							# 返回值中拿到关系上的属性
							{rel_fields, select_acc} = case Map.get(select, rel_obj) do
								nil ->
									{"", Map.delete(select_acc, field_obj)}
								rel_values ->
									v1 = map_join(rel_values, ", ", rel_obj)
									v = if attach do
										rel_obj <> ": {" <> v1 <> "}"
									else
										v1
									end
									v = if field_values == "" do
										v
									else
										", " <> v
									end
									{v, MapLib.drop_key(select_acc, [field_obj, rel_obj])}
							end
							v = field_values <> rel_fields
							{kv_str, obj_with_acc} =
								if type == :one_to_many do
									str = if is_map? do
										"{" <> v <> "}"
									else
										case value do
											[f | []] ->
												field_obj <> "." <> f
											_ ->
												"{" <> v <> "}"
										end
									end
									kv_str = field_obj <> ": " <> field_obj
									with_str = " CASE WHEN NOT #{field_obj} IS NULL" <>
														 " THEN COLLECT(#{str}) ELSE [] END AS #{field_obj}"
									{kv_str, with_str}
								else
									if attach do
										kv_str = field_obj <> ": " <> field_obj
										with_str = " CASE WHEN NOT #{field_obj} IS NULL THEN {"
										<> field_values <> rel_fields <> "} ELSE NULL END AS #{field_obj}"
										{kv_str, with_str}
									else
										{v, field_obj}
									end
								end
							{acc1, [kv_str | kv_acc], select_acc, [obj_with_acc | with_acc]}
						_ ->
							{acc1, [field_values | kv_acc], Map.delete(select_acc, field_obj), with_acc}
					end
				field, {acc1, kv_acc, select_acc, with_acc} when is_binary(field) ->
					{acc1, with_acc} = case get_filed(target_mod, target_obj, fields, field) do
						{str, with_obj} ->
							{[str | acc1], [with_obj | with_acc]}
						nil ->
							{acc1, with_acc}
						str ->
							{[str | acc1], with_acc}
					end
					{acc1, kv_acc, List.delete(select_acc, field), with_acc}
				field, _ ->
					PrivateCandy.debug_remind(__ENV__, "#{target_obj} 属性错误, key: #{field}")
			end
		)
		#  遍历select中剩下的属性, 不存在, 或者该属性是关系对象属性
		{rest_select_acc, with_acc} = Enum.reduce(
			rest_select,
			{[], with_acc},
			fn
				{rel_key, list}, {rel_acc, with_acc2} ->
					v = map_join(list, ", ", rel_key)
					rel_attach = Map.get(fields, rel_key).relation_object
					case Map.get(fields, rel_attach) do
						%{type: :one_to_many} ->
							rel_key <> ": COLLECT({#{v}})"
							{[rel_key <> ": " <> rel_key | rel_acc], ["COLLECT({#{v}}) AS #{rel_key}" | with_acc2]}
						_ ->
							if attach do
								{[rel_key <> ": {" <> v <> "}" | rel_acc], [rel_key | with_acc2]}
							else
								{rel_acc, [rel_key | with_acc2]}
							end
					end
				v, acc ->
					acc
			end
		)
		str = rest_select_acc ++ acc1 ++ kv_acc
					|> Enum.join(", ")
		str != "" || PrivateCandy.debug_remind(__ENV__, "返回不能为空")
		{with_acc, "{#{str}}"}
	end


	defp get_filed(target_mod, target_obj, fields, field) do
		case Map.get(fields, field) do
			nil when field == target_obj ->
				{primary_key, _} = target_mod.primary_key()
				field <> ": " <> target_obj <> "." <> primary_key
			%{mod: mod, type: type} ->
				{primary_key, _} = mod.primary_key()
				if type == :one_to_many do
					{"#{field}: " <> field, "[x IN COLLECT(#{field}.#{primary_key}) WHERE NOT x IS NULL] AS #{field}"}
				else
					{field <> ": " <> field <> "." <> primary_key, field}
				end
			%{} ->
				field <> ": " <> target_obj <> "." <> field
			nil ->
				PrivateCandy.debug_remind(__ENV__, "#{target_obj} 没有该属性, #{field}")
			_ ->
				field
		end
	end

	defp map_join(enum, joiner, target_key) do
		Enum.map_join(
			enum,
			joiner,
			fn
				{field, bool} when is_boolean(bool) ->
					field <> ": " <> target_key <> "." <> field
				{field, alias} ->
					alias <> ": " <> target_key <> "." <> field
				field ->
					field <> ": " <> target_key <> "." <> field
			end
		)
	end

	def get_point(dir, rel_label) do
		get_point(dir, rel_label, "")
	end
	def get_point(dir, rel_label, rel_attr) do
		get_point(dir, "", rel_label, rel_attr)
	end
	def get_point("->", rel_name, rel_label, rel_attr) do
		"-[#{rel_name}:#{rel_label}#{rel_attr}]->"
	end
	def get_point("<-", rel_name, rel_label, rel_attr) do
		"<-[#{rel_name}:#{rel_label}#{rel_attr}]-"
	end


	def foreach(obj, rel_obj, rel_name, relation_attr) do
		" FOREACH(r IN #{rel_obj}s | MERGE (#{obj})-[:#{rel_name}#{relation_attr}]->(r)) "
	end

	def where(:in, obj, field, value) do
		"WHERE #{obj}.#{field} IN #{inspect(value)} "
	end
	def where(:not_in, obj, field, value) do
		"WHERE NOT #{obj}.#{field} IN #{inspect(value)} "
	end

	def where([]) do
		""
	end
	def where(values) do
		" WHERE " <> Enum.join(values, " AND ")
	end

	def match(obj) do
		"MATCH " <> object(obj)
	end
	def match(obj, label) do
		"MATCH " <> object(obj, label)
	end
	def match(obj, label, primary_key, key_value) do
		"MATCH " <> object(obj, label, primary_key, key_value)
	end


	def object(obj) do
		"(#{obj}) "
	end
	def object(obj, mod) when is_atom(mod) do
		"(#{obj}:#{mod.label()}) "
	end
	def object(obj, label) do
		"(#{obj}:#{label}) "
	end
	def object(obj, mod, primary_key, key_value) when is_atom(mod) do
		"(#{obj}:#{mod.label()}{#{primary_key}: #{inspect(CypherLib.to_str(key_value))}}) "
	end
	def object(obj, label, primary_key, key_value) do
		"(#{obj}:#{label}{#{primary_key}: #{inspect(CypherLib.to_str(key_value))}}) "
	end


	def collect(obj) do
		"COLLECT(#{obj}) AS #{obj}s"
	end

	def optional_match_obj(obj) do
		"OPTIONAL " <> match(obj)
	end
	def optional_match_obj(obj, label) do
		"OPTIONAL " <> match(obj, label)
	end
	def optional_match_obj(obj, label, primary_key, key_value) do
		"OPTIONAL " <> match(obj, label, primary_key, key_value)
	end


	def exists(obj1, obj2) do
		"EXISTS((#{obj1})--(#{obj2}))"
	end

	def not_exist(obj1, obj2) do
		"NOT " <> exists(obj1, obj2)
	end

	def delete(obj) do
		"DELETE #{obj}"
	end
	def detach_delete(obj) do
		"DETACH " <> delete(obj)
	end

	def or_(str1, str2) do
		"(#{str1}" <> " OR " <> str2 <> ") "
	end

	def with_([]) do
		""
	end
	def with_(with) do
		" WITH " <> Enum.join(with, ", ")
	end

	def merge(mod, args, create) do
		"MERGE " <> CypherLib.insert_cypher(mod, args) <> create
	end

	def get_page_str(rel_obj, select, total_page, page) do
		page_size = AppConfigure.fetch(:page_size)
		total_page_str =
			#			if page == 1 && total_page do
			if true do
				", (COUNT(#{rel_obj}) + #{page_size} - 1) / #{page_size} AS total_page, COUNT(#{rel_obj}) as total_count"
			else
				", NULL AS total_page, NULL AS total_count"
			end
		if page != false do
			", #{page} as page, COLLECT(#{select})[#{(page - 1) * page_size}..#{page_size * page}] AS list"
		else
			", #{page} as page, COLLECT(#{select})[0..100] AS list"
		end <> total_page_str
	end

end
